今天要介紹 GraphQL 三大支柱之二的 Mutation 。相比 Query 負責資料的取得,凡是資料更改或新增都屬於 Mutation 的負責範圍。
與 Query 相同的是, Mutation 一樣是整個 GraphQL Schema 的 entry point ,並且也需要先在後端 Schema 定義好才能供前端 query ,所以我會先介紹如何在 Schema 中定義 Mutation ,然後才講解如何使用 Mutation Query 。
開始前我們先在原先的 Schema 中加入一個新的 Post
Type 並在 User
Type 中新增 posts
fields ,同時也在 resolver 加入相關 fucntion
(為 demo 方便,我先移除一些較不重要的欄位)
一筆貼文內容有 id, 作者 id (authorId), 標題 (title), 內文 (content) 以及按讚者 id (likeGiverIds)
const posts = [
{ id: 1, authorId: 1, title: "Hello World!", content: "This is my first post.", likeGiverIds: [2] },
{ id: 2, authorId: 2, title: "Good Night", content: "Have a Nice Dream =)", likeGiverIds: [2, 3] },
{ id: 3, authorId: 1, title: "I Love U", content: "Here's my second post!", likeGiverIds: [] },
];
const typeDefs = gql`
type User { ... }
"""
貼文
"""
type Post {
"識別碼"
id: ID!
"作者"
author: User
"標題"
title: String
"內容"
content: String
"按讚者"
likeGivers: [User]
}
type Query { ... }
`;
以及加上 Resolver:
// Helper Functions
const findUserById = id => users.find(user => user.id === id);
const findUserByName = name => users.find(user => user.name === name);
const filterPostsByAuthorId = authorId =>
posts.filter(post => post.authorId === authorId);
// 1. 新增 User.posts field Resovler
// 2. 新增 Post Type Resolver 及底下的 field Resolver
const resolvers = {
Query: { ... },
User: {
...,
// 1. User.parent field resolver, 回傳屬於該 user 的 posts
posts: (parent, args, context) => {
// parent.id 為 userId
return filterPostsByAuthorId(parent.id);
}
},
// 2. Post type resolver
Post: {
// 2-1. parent 為 post 的資料,透過 post.likeGiverIds 連接到 users
likeGivers: (parent, args, context) => {
return parent.likeGiverIds.map(id => findUserById(id));
},
// 2-2. parent 為 post 的資料,透過 post.author
author: (parent, args, context) => {
return findUserById(parent.authorId);
}
}
};
有了一個文章系統後,讓我們走下去:
首先來看我們需要哪些 Mutation
addPost(title, content)
: 新增文章。參數為 title (標題)、 content (內文)likePost(postId)
: 喜歡文章。參數為該 post 的 id接下來需要做兩件事,將以上 mutation 加入 Schema 的 Mutation 定義以及新增相關的 Resolver function。
進入程式:
(上面提過的程式會先忽略)
const typeDefs = gql`
type User { ... }
type Post { ... }
type Query { ... }
# Mutation 定義
type Mutation {
"新增貼文"
addPost(title: String!, content: String!): Post
"貼文按讚 (收回讚)"
likePost(postId: ID!): Post
}
`;
接下來是 Resolver function。
因為 mutation 都預設是目前使用者在做操作 (畢竟使用者 A 可以幫 B 新增文章也很怪) ,所以這裡會先在上面定義 meId
來代表目前使用者的 id ,之後介紹到 login 時會有更好的應用方式。
const meId = 1;
const findPostById = id => posts.find(post => post.id === id);
const resolvers = {
Query: { ... },
// Mutation Type Resolver
Mutation : {
addPost: (root, args, context) => {
const { title, content } = args;
// 新增 post
posts.push({
id: posts.length + 1,
authorId: meId,
title,
content,
likeGivers: []
});
// 回傳新增的那篇 post
return posts[posts.length - 1];
},
likePost: (root, args, context) => {
const { postId } = args;
const post = findPostById(postId);
if (!post) throw new Error(`Post ${psotId} Not Exists`);
if (post.likeGiverIds.includes(meId)) {
// 如果已經按過讚就收回
const index = post.likeGiverIds.findIndex(v => v === userId);
post.likeGiverIds.splice(index, 1);
} else {
// 否則就加入 likeGiverIds 名單
post.likeGiverIds.push(meId);
}
return post;
},
},
User: { ... },
Post: { ... }
};
可以看到, Mutation 的定義方式與 Query 一模一樣且 field 名稱也要對上。
而 Mutation 有趣點在於執行操作後它還可以回傳資料,讓 client 端做完 muation 後可以直接得到更新後的資料且 field 可自行挑選。
此外如果 Mutation field 宣告參數時, Scalar Type 的參數可以直接宣告,但如果要使用 Object 格式的參數,與 query 不同,需要額外再宣告 Input Object Type 才能使用 (下面會再介紹) 。
接下來是 Query Part ,格式上可參考下圖。
接著就讓我們來寫 mutation query
# Operation Type 為 mutation 時不可省略
mutation {
addPost(title: "Mutation Is Awesome", content: "Adding Post is like a piece of cake") {
id
title
author {
name
}
}
}
得到的 Response
{
"data": {
"addPost": {
"id": "4",
"title": "Mutation Is Awesome",
"author": { "name": "Fong" }
}
}
}
可見圖:
title
為 Non-Null String (必填)、 content
為 Nullable String (選填)。若格式不符合會直接吐 Error ,連 Resolver 都進不去。Query Fields 是平行執行的,然而 Mutation 則是一筆一筆照順序執行
如果你已經成功發出 mutation ,可以試試 query 自己的 post 看貼文數量是否有增加 ! 如圖:
如果你懶得打扣可以看看我的 GraphQL Playground Example
經驗談: 說實在, Mutation 與 Query 兩者最主要的差別在語意上,如果今天你使用 query 然後後台把資料大改一通也沒有人會阻止你。所以使用 GraphQL 時務必要遵守好規範,對於資料的修改都要非常謹慎,不然自己挖的坑自己同事要負責填。
當參數列所需的資料越來越複雜、到時候長度就會讓你看不完...
這時 Input Type 提供了 Object Type 形式的 Argument ,讓我們來看如何使用。可參考下圖。
接下來看範例~~
const typeDefs = gql`
...
input AddPostInput {
title: String!
content: String
}
Mutation {
addPost(input: AddPostInput!): Post
}
`;
這邊非常重要, Input Object Type 與 Object Type 完全不同,一個是傳入 Argument 作為 Input ,一個是用於資料索取展示。
然後是 Resolver 部分:
const resolvers = {
...,
Mutation: {
...,
// 需注意!args 打開後第一層為 input ,再進去一層才是 title, content
addPost: (root, args, context) => {
const { input } = args;
const { title, content } = input;
const newPost = {
id: posts.length + 1,
authorId: meId,
title,
content,
likeGivers: []
};
posts.push(newPost);
return newPost;
},
},
}
接著在 GraphQL Playground 輸入 mutation:
# Operation Type 為 mutation 時不可省略
mutation AddPostAgain ($input: AddPostInput) {
addPost(input: $input) {
id
title
author {
name
}
}
}
---
Variables
{
"input": {
"title": "Input Object Is Awesome",
"content": "ZZZZZZZZZZZZZ"
}
}
就會得到跟以上一樣的回覆
{
"data": {
"addPost": {
"id": "5",
"title": "Input Object Is Awesome",
"author": { "name": "Fong" }
}
}
}
Input Object Type 就是這麼簡單~~
經驗談: 公司剛開發時對於 GraphQL 的掌握度還不高,所以程式中 input object type 與 object type 的命名相似且組成的 field 也幾近相同 (ex: 同時會有 NewOrder
, UpdateOrder
, Order
),使得我一開始學習上常常搞混兩者,後來加入一些規範及重構後才漸漸將兩者的概念分清楚,這邊想講一些推薦的 convention。
命名部分,推薦每一支 mutation 都新增一支專屬的 input object type ,並可考慮在命名上採用 [mutation name] + Input
的形式 (如addPost
Input )方便辨認。
另外一些 field 如 id
, createdAt
, createdBy
, updatedBy
等等可透過 server 自動給值的欄位就可以不用出現在 input object type 中,統一讓 server 去計算,減少需要考慮的 field 的數量。
有了前面的基礎,相信學習 Mutation 也是輕鬆就上手!不過這幾天一下子吸收了這麼大量的知識,可能已經開始吃不消了,所以明天我們就來依靠現有的技術來打造一個實用的 GraphQL API 吧!
那要做什麼應用呢?那就做一個部落格社交系統好了!
Reference:
Mutation 中 content 似乎少了 type
type Mutation {
"新增貼文"
addPost(title: String!, content): Post
"貼文按讚 (收回讚)"
likePost(postId: ID!): Post
}
感謝提醒!
不好意思,請問在 GraphQL Playground 要如何輸入 input 的 Variables 才能得到回覆呢? 本來是直接照著輸入但是會報 Unexpected Name Variables 的 error,爬了 GraphQL 的文件的 Mutations 沒有看到前端要如何輸入變數方可拿到回覆的說明,可否請大大為前端菜雞解惑,感激不盡!
大大你好,不好意思我耍笨 XD
已經找到要在哪裡輸入變數了!
likePost: (root, args, context) => {
const { postId } = args;
const post = findPostById(postId);
if (!post) throw new Error(`Post ${psotId} Not Exists`);
if (post.likeGiverIds.includes(meId)) {
// 如果已經按過讚就收回
const index = post.likeGiverIds.findIndex(v => v === userId);
post.likeGiverIds.splice(index, 1);
} else {
// 否則就加入 likeGiverIds 名單
post.likeGiverIds.push(meId);
}
return post;
}
第四行 psotId 應該是 postId
第七行 userId 應該是 meId